RDD - Accumulator Variables
When writing a distributed application, you often need to track global metrics across the entire clustersuch as counting the number of blank or malformed lines in a 10TB dataset, tracking specific transaction types, or measuring error frequencies.
If you attempt to use a standard Python global variable inside your Spark operations, it will fail because executors run in separate JVM processes and cannot send updates back to the Driver program.
To solve this, Spark provides Accumulator Variablesshared variables that worker tasks can only write to (via addition), and only the Driver program can read.
This guide provides a detailed exploration of Accumulators, their execution rules, and the common lazy evaluation pitfalls to avoid.
1. Why Standard Python Variables Fail
Let's look at a common mistake:
# WARNING: THIS WILL NOT WORK!
counter = 0
rdd = sc.parallelize([1, 2, 3, 4])
def increment_count(x):
global counter
counter += 1 # Increments local copy on the executor JVM!
return x
rdd.map(increment_count).collect()
print("Global Counter:", counter)
# Output: Global Counter: 0 (The driver's counter is completely unaffected!)
The Accumulator Solution
An Accumulator acts as a secure, distributed collector. Spark takes care of initializing a local accumulator copy on each executor, gathering their final values automatically, and merging them at the Driver program using standard addition.
graph TD
subgraph Driver["Driver Program (Master Node)"]
A["sc.accumulator(0)"] -->|Deploy copy| E1["Executor 1 (Counter=0)"]
A -->|Deploy copy| E2["Executor 2 (Counter=0)"]
E1 -->|Adds +1, +1| E1_val["Final Local: 2"]
E2 -->|Adds +1| E2_val["Final Local: 1"]
E1_val & E2_val -->|Automatic Merge| A_final["Final Driver Value: 3"]
end
style Driver fill:#e1f5fe,stroke:#039be5,stroke-width:2px;
2. PySpark Code Examples
Setup Spark Session
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("Day01 Accumulator Variables") \
.master("local[*]") \
.getOrCreate()
sc = spark.sparkContext
A. Counting Malformed Rows in a Log File
Let's parse a dataset of logs and use an Accumulator to count the number of invalid rows without triggering an expensive network shuffle:
# 1. Initialize a numeric Accumulator starting at 0
malformed_rows_counter = sc.accumulator(0)
# 2. Raw logs dataset (some rows are empty or formatted incorrectly)
raw_logs = sc.parallelize([
"2026-05-23 INFO: Login success",
"", # Malformed: Empty row
"2026-05-23 ERROR: Disk full",
"INVALID_FORMAT_ROW", # Malformed: Missing log level
"2026-05-23 INFO: Logging out"
], numSlices=2)
# 3. Parsing function that increments accumulator on error
def parse_logs(line):
# Reference the global accumulator
global malformed_rows_counter
# Check if empty row
if not line.strip():
malformed_rows_counter.add(1) # Increment the accumulator by 1
return None
parts = line.split(" ")
if len(parts) < 3:
malformed_rows_counter.add(1) # Increment the accumulator by 1
return None
timestamp = parts[0]
log_level = parts[1].replace(":", "")
message = " ".join(parts[2:])
return (timestamp, log_level, message)
# 4. Apply the map transformation (lazy evaluation!)
parsed_logs_rdd = raw_logs.map(parse_logs)
# 5. Filter out the None results
valid_logs_rdd = parsed_logs_rdd.filter(lambda row: row is not None)
# 6. Accumulators are LAZY! Value is still 0 before an Action
print("Counter before Action:", malformed_rows_counter.value) # Output: 0
# 7. Trigger the computation using an Action (collect)
valid_logs = valid_logs_rdd.collect()
print("
--- Processing Completed ---")
print("Valid Parsed Logs:")
for log in valid_logs:
print(f" {log}")
# 8. Query the accumulator's final merged value on the Driver
total_malformed = malformed_rows_counter.value
print("
Malformed Rows Counted Globally:", total_malformed)
# Expected Output:
# Counter before Action: 0
# Valid Parsed Logs:
# ('2026-05-23', 'INFO', 'Login success')
# ('2026-05-23', 'ERROR', 'Disk full')
# ('2026-05-23', 'INFO', 'Logging out')
#
# Malformed Rows Counted Globally: 2
3. The Lazy Transformation Gotcha: Double-Counting
Because Spark transformations are lazy, updates to accumulators inside transformations (like map or filter) can be unreliable if you execute multiple actions on the same RDD.
The Double-Counting Scenario:
If you run rdd.collect(), the accumulator is updated. If you run rdd.count() next, Spark recomputes the entire lineage graph from scratch, causing the accumulator to run a second time and double its count!
# 1. Create a fresh accumulator
double_counter = sc.accumulator(0)
# 2. Lazy transformation that increments the accumulator
numbers_rdd = sc.parallelize([1, 2, 3]).map(lambda x: double_counter.add(1))
# 3. Action 1: Triggers map (increments counter to 3)
numbers_rdd.collect()
print("Accumulator after Action 1:", double_counter.value) # 3
# 4. Action 2: Triggers map AGAIN (increments counter to 6!)
numbers_rdd.count()
print("Accumulator after Action 2 (Double-counted!):", double_counter.value) # 6
How to Prevent Double-Counting:
- Use Actions for Updates: Increment accumulators inside the
foreach(func)action. Spark guarantees that each record will be processed exactly once, and re-execution will not occur unless node failures force it. - Apply Caching: Call
.cache()on the RDD before running your first action. Subsequent actions will read directly from the cached RAM, bypassing the transformation lineage and preventing re-increments.
4. Key Rules for Accumulators
- Write-Only on Executors: Worker tasks can only write to the accumulator using the
.add()method. They cannot read the accumulator's value. Attempting to querymalformed_rows_counter.valueinside a map/filter transformation will throw a runtime compilation error! - Read-Only on Driver: The Driver program is the only process that can read the accumulator value using
.value. - Custom Accumulators: Spark allows you to create custom accumulators for non-numeric data types (e.g., aggregating lists or custom classes) by extending the
AccumulatorParamclass.